LiveActivity

The LiveActivity API enables you to display real-time, dynamic information from your script on the Lock Screen and, where supported, in the Dynamic Island on iOS devices. It provides a structured interface to start, update, and end Live Activities, and observe their state throughout their lifecycle.

This document provides a complete guide to using the LiveActivity API in the Scripting app, including:

  • Core concepts and lifecycle
  • How to register a Live Activity UI
  • How to start, update, and end Live Activities
  • UI layout for Dynamic Island and Lock Screen
  • Full TypeScript/TSX examples
  • Detailed descriptions of every type and option

The API wraps Apple’s ActivityKit and brings it into the Scripting environment with a React-style UI building approach.


1. Understanding Live Activities

A Live Activity can appear in the following regions:

  • Lock Screen
  • Dynamic Island (iPhone 14 Pro and later)
  • Banner-style presentation on devices without Dynamic Island

Live Activities are used for time-based and progress-based information, such as:

  • Timers
  • Fitness progress
  • Delivery tracking
  • Countdowns and reminders
  • Real-time status updates

In Scripting, each Live Activity consists of:

  1. contentState (a JSON-serializable object that updates over time)
  2. UI Builder (a function that produces TSX UI for each state)

2. Live Activity State Types

1type LiveActivityState = "active" | "dismissed" | "ended" | "stale";
State Description
active The Live Activity is visible and can receive content updates.
stale The Live Activity is out of date. The system expects an update.
ended The Live Activity ended but may remain visible for up to four hours or a user-defined time.
dismissed The Live Activity is no longer visible.

3. LiveActivityDetail Type

1type LiveActivityDetail = {
2  id: string;
3  state: LiveActivityState;
4};

Represents a summary of each active Live Activity.


4. Live Activity UI Types

4.1 LiveActivityUIProps

1type LiveActivityUIProps = {
2  content: VirtualNode;
3  compactLeading: VirtualNode;
4  compactTrailing: VirtualNode;
5  minimal: VirtualNode;
6  children: VirtualNode | VirtualNode[];
7};

These regions correspond to ActivityKit’s UI areas:

Property Region
content Lock Screen and non–Dynamic Island devices
compactLeading Leading area of compact Dynamic Island
compactTrailing Trailing area of compact Dynamic Island
minimal The smallest pill-style display
children The expanded Dynamic Island layout (multiple regions)

5. Registering a Live Activity UI

Live Activities must be registered inside a standalone file such as live_activity.tsx.

1import { LiveActivity, LiveActivityUI, LiveActivityUIBuilder } from "scripting";
2
3export type State = {
4  mins: number;
5};
6
7function ContentView(state: State) {
8  return (
9    <HStack activityBackgroundTint={{ light: "clear", dark: "clear" }}>
10      <Image systemName="waterbottle" foregroundStyle="systemBlue" />
11      <Text>{state.mins} minutes left until the next drink</Text>
12    </HStack>
13  );
14}
15
16const builder: LiveActivityUIBuilder<State> = (state) => {
17  return (
18    <LiveActivityUI
19      content={<ContentView {...state} />}
20      compactLeading={
21        <HStack>
22          <Image systemName="clock" />
23          <Text>{state.mins}m</Text>
24        </HStack>
25      }
26      compactTrailing={<Image systemName="waterbottle" foregroundStyle="systemBlue" />}
27      minimal={<Image systemName="clock" />}>
28      <LiveActivityUIExpandedCenter>
29        <ContentView {...state} />
30      </LiveActivityUIExpandedCenter>
31    </LiveActivityUI>
32  );
33};
34
35export const MyLiveActivity = LiveActivity.register("MyLiveActivity", builder);

6. Using a Live Activity in Your Script

1import {
2  Button,
3  Text,
4  VStack,
5  Navigation,
6  NavigationStack,
7  useMemo,
8  useState,
9  LiveActivityState,
10  BackgroundKeeper,
11} from "scripting";
12
13import { MyLiveActivity } from "./live_activity";
14
15function Example() {
16  const dismiss = Navigation.useDismiss();
17  const [state, setState] = useState<LiveActivityState>();
18
19  const activity = useMemo(() => {
20    const instance = MyLiveActivity();
21
22    instance.addUpdateListener((s) => {
23      setState(s);
24      if (s === "dismissed") {
25        BackgroundKeeper.stop();
26      }
27    });
28
29    return instance;
30  }, []);
31
32  return (
33    <NavigationStack>
34      <VStack
35        navigationTitle="Live Activity Example"
36        navigationBarTitleDisplayMode="inline"
37        toolbar={{
38          cancellationAction: <Button title="Done" action={dismiss} />,
39        }}>
40        <Text>Activity State: {state ?? "-"}</Text>
41
42        <Button
43          title="Start Live Activity"
44          disabled={state != null}
45          action={() => {
46            let count = 5;
47            BackgroundKeeper.keepAlive();
48
49            activity.start({ mins: count });
50
51            function tick() {
52              setTimeout(() => {
53                count -= 1;
54                if (count === 0) {
55                  activity.end({ mins: 0 });
56                  BackgroundKeeper.stop();
57                } else {
58                  activity.update({ mins: count });
59                  tick();
60                }
61              }, 60000);
62            }
63
64            tick();
65          }}
66        />
67      </VStack>
68    </NavigationStack>
69  );
70}
71
72async function run() {
73  await Navigation.present(<Example />);
74  Script.exit();
75}
76
77run();

7. LiveActivity Class API Reference

7.1 start(contentState, options?)

1start(contentState: T, options?: LiveActivityOptions): Promise<boolean>

Starts a Live Activity.

LiveActivityOptions

1type LiveActivityOptions = {
2  staleDate?: number | Date;
3  relevanceScore?: number;
4};
  • staleDate: Timestamp(ms) or Date object at which the activity becomes stale
  • relevanceScore: Determines which Live Activity is prioritized in the Dynamic Island

7.2 update(contentState, options?)

1update(contentState: T, options?: LiveActivityUpdateOptions)

LiveActivityUpdateOptions

1type LiveActivityUpdateOptions = {
2  staleDate?: number | Date;
3  relevanceScore?: number;
4  alert?: {
5    title: string;
6    body: string;
7  };
8};

Alerts appear on Apple Watch when sending an update.


7.3 end(contentState, options?)

1end(contentState: T, options?: LiveActivityEndOptions)

LiveActivityEndOptions

1type LiveActivityEndOptions = {
2  staleDate?: number | Date
3  relevanceScore?: number
4  dismissTimeInterval?: number
5}

Rules for dismissal (seconds):

  • Not provided: default system retention (up to 4 hours)
  • <= 0: remove immediately
  • > 0: remove after the specified interval

7.4 Reading Activity State

1getActivityState(): Promise<LiveActivityState | null>

7.5 Listening for State Changes

1addUpdateListener(listener);
2removeUpdateListener(listener);

Triggered when the Live Activity transitions between:

  • active → stale
  • active → ended
  • ended → dismissed

7.6 Static Methods

1static areActivitiesEnabled(): Promise<boolean>
2static getAllActivities(): Promise<LiveActivityDetail[]>
3static getAllActivitiesIds(): Promise<string[]>
4static getActivityState(activityId: string)
5static from(activityId, name)
6static endAllActivities(options?)

8. UI Components for Expanded Layout

Component Description
LiveActivityUI Root layout container
LiveActivityUIExpandedLeading Leading region of expanded layout
LiveActivityUIExpandedTrailing Trailing region
LiveActivityUIExpandedCenter Center region
LiveActivityUIExpandedBottom Bottom region

These components help structure the expanded Dynamic Island.


9. Best Practices

9.1 contentState must be JSON-serializable

The following are not allowed:

  • Functions
  • Date objects (must use timestamps)
  • Class instances
  • Non-serializable structures

9.2 Live Activity registration must be in a standalone file

This is required due to UI compilation and ActivityKit rules.

9.3 Live Activities survive script termination

If your script needs to keep running (e.g., timers), use:

1BackgroundKeeper.keepAlive();

10. Minimal Example

1const activity = MyLiveActivity();
2
3await activity.start({ mins: 10 });
4
5await activity.update({ mins: 5 });
6
7await activity.end({ mins: 0 }, { dismissTimeInterval: 0 });

11. Notes

  • Live Activity starts asynchronously. You need to wait for start to return true before calling update and end.
  • Live Activity cannot access documents and iCloud directories. If you want to access files or render images, you must save them to FileManager.appGroupDocumentsDirectory. For example, to render an image, you save it to FileManager.appGroupDocumentsDirectory, then use <Image filePath={Path.join(FileManager.appGroupDocumentsDirectory, 'example.png')} /> to render it.
  • Live Activity can access the Storage data shared with the app.